package splar.core.fm;

//EDITED BY VITOR ALMEIDA BARROS
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Stack;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import splar.core.constraints.BooleanVariableInterface;
import splar.core.constraints.CNFClause;
import splar.core.constraints.PropositionalFormula;
import splar.core.constraints.parsing.CNFClauseParseException;
import splar.core.constraints.parsing.CNFClauseParser;

public class XMLFeatureModel extends FeatureModel {

	private static final long serialVersionUID = -377304154041387562L;
	
	public static final int USE_VARIABLE_NAME_AS_ID = 10;
	public static final int SET_ID_AUTOMATICALLY = 20;

	private String fileName;
	private int idCounter;
	private int idCreationStrategy;

	public XMLFeatureModel(String fileName) {
		this(fileName, SET_ID_AUTOMATICALLY);
	}

	public XMLFeatureModel(String fileName, int idCreationStrategy) {
		super();
		this.fileName = fileName;
		this.idCreationStrategy = idCreationStrategy;
	}

	public int getIDCreationStrategy() {
		return idCreationStrategy;
	}

	@Override
	protected FeatureTreeNode createNodes() throws FeatureModelException {

		FeatureTreeNode rootNode = null;

		idCounter = 0;
		// if model is already loaded just return the feature model root node
		if (getRoot() != null) {
			rootNode = getRoot();
		}
		// otherwise, load it from the XML feature model file
		else {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			try {
				// Using factory get an instance of document builder
				DocumentBuilder db = dbf.newDocumentBuilder();

				// parse using builder to get DOM representation of the XML file
				Document doc = db.parse(fileName);

				// get the root element
				Element rootEle = doc.getDocumentElement();
				String featureModelName = rootEle.getAttribute("name");

				if (featureModelName == null || featureModelName.equals("")) {
					throw new FeatureModelException("Missing mandatory feature model name.");
				}

				setName(featureModelName);

				// parse metadata, if section exists
				NodeList metalist = rootEle.getElementsByTagName("meta");
				if (metalist.getLength() > 0) {
					// System.out.println("metadata section found!");
					Element metaElement = (Element) metalist.item(0);
					readMetaData(metaElement);
				}
				// else {
				// System.out.println("No metadata section found!");
				// }

				// parse feature tree
				NodeList list = rootEle.getElementsByTagName("feature_tree");
				Element featureTreeEle = (Element) list.item(0);
				rootNode = parseFeatureTree(featureTreeEle.getTextContent());

				// parse constraints
				list = rootEle.getElementsByTagName("constraints");
				Element constraintsEle = (Element) list.item(0);
				String constraintsText = constraintsEle.getTextContent();
				parseConstraints(constraintsText);
			} catch (FeatureModelException e) {
				throw e;
			} catch (ParserConfigurationException pce) {
				throw new FeatureModelException("XML error parsing SXFM file: " + pce.getMessage(), pce);
			} catch (SAXException se) {
				throw new FeatureModelException("XML SAX error parsing SXFM file: " + se.getMessage(), se);
			} catch (IOException ioe) {
				throw new FeatureModelException("Error reading SXFM file: " + ioe.getMessage(), ioe);
			}
		}
		return rootNode;
	}

	protected void readMetaData(Element metaElement) {
		// System.out.println(metaElement.getNodeName());
		NodeList nodeList = metaElement.getElementsByTagName("data");
		for (int i = 0; i < nodeList.getLength(); i++) {
			Element element = (Element) nodeList.item(i);
			String name = element.getAttribute("name");
			String value = element.getTextContent();
			// System.out.println(">> " + name + ": " + value);
			addMetaData(name, value);
		}
	}

	protected void parseConstraints(String constraints) throws FeatureModelException {

		CNFClauseParser cnfClauseParser = new CNFClauseParser();

		Scanner scanner = new Scanner(constraints);
		String line = "";
		while (scanner.hasNextLine()) {
			line = scanner.nextLine().trim();
			if (line.trim().length() > 0) {
				int index1 = line.indexOf(":");
				if (hasId(index1)) {
					String constraintName = line.substring(0, index1).trim();
					String constraintFormula = line.substring(index1 + 1).trim();
					try {
						CNFClause cnfClause = cnfClauseParser.parse(constraintFormula);
						for (BooleanVariableInterface var : cnfClause.getVariables()) {
							if (getNodeByID(var.getID()) == null) {
								throw new FeatureModelException("Error parsing extra constraint labelled '" + constraintName
										+ "' (variable id '" + var.getID()
										+ "' used in the formula is not defined in the feature tree).");
							}
						}
						addConstraint(new PropositionalFormula(constraintName, cnfClause.toString2()));
					} catch (CNFClauseParseException e1) {
						throw new FeatureModelException("Error parsing extra constraint labelled '" + constraintName + "' ("
								+ e1.getMessage() + "').", e1);
					} catch (FeatureModelException e2) {
						throw e2;
					} catch (Exception e3) {
						throw new FeatureModelException("Error parsing extra constraint labelled '" + constraintName
								+ "' (Line: " + line + "').", e3);
					}
				}
			}
		}
	}

	protected FeatureTreeNode parseFeatureTree(String featureTree) throws IOException, FeatureModelException {
		FeatureTreeNode rootNode = null;
		Stack<FeatureTreeNode> nodeStack = new Stack<FeatureTreeNode>();

		// System.out.println(featureTree);
		LineNumberReader reader = new LineNumberReader(new StringReader(featureTree));
		String line = skipBlanks(reader);

		List<FeatureTreeNode> processedNodes = new ArrayList<FeatureTreeNode>();

		try {
			while (line != null) {
				FeatureTreeNode node = parseNode(line);

				while (processedNodes.contains(node)) {
					node.setID("_" + node.getID());
				}

				processedNodes.add(node);

				int level = countTabs(line);

				// root node goes to the Stack
				if (rootNode == null && level == 0) {
					rootNode = node;
					nodeStack.push(node);
				} else {
					int curLevel = nodeStack.size() - 1;
					// child node found
					if (level > curLevel) {
						FeatureTreeNode parent = nodeStack.peek();
						parent.add(node);
						nodeStack.push(node);
						// TODO: check if group cardinality is within boundaries
					} else if (level == curLevel) {
						nodeStack.pop();
						FeatureTreeNode parent = nodeStack.peek();
						parent.add(node);
						nodeStack.push(node);
					}
					// another branch found
					else {
						int countPops = curLevel - level + 1;
						for (int i = 0; i < countPops; i++) {
							nodeStack.pop();
						}
						FeatureTreeNode parent = nodeStack.peek();
						parent.add(node);
						nodeStack.push(node);
					}
				}

				nodesMap.put(node.getID(), node);

				line = skipBlanks(reader);
			}
		} catch (FeatureModelException e1) {
			throw e1;
		} catch (Exception e2) {
			throw new FeatureModelException("Error parsing Feature Tree on line '" + line.trim() + "': " + e2.toString());
		}

		return rootNode;
	}

	private int countTabs(String line) {
		int count = 0;
		int index = line.indexOf('\t');
		while (hasId(index)) {
			count++;
			index = line.indexOf('\t', index + 1);
		}
		return count;
	}

	// ADAPTED BY REGIS HIDEKI HATTORI
	private FeatureTreeNode parseNode(String line) throws FeatureModelException {

		FeatureTreeNode node = null;
		int lineAfterTabs = line.indexOf(':');

		int openParentheses = line.indexOf("(");
		int closeParentheses = line.indexOf(")");
		int openBrackets = line.indexOf("[");
		int firstVerticalBar = line.indexOf("|");
		int secondVerticalBar = firstVerticalBar + 1 + line.substring(firstVerticalBar + 1).indexOf("|");
		
		String nodeType = line.substring(lineAfterTabs + 1, lineAfterTabs + 2).trim().toUpperCase();

		verifyIfIsValid(line, nodeType);

		String featureName = "";
		String featureID = "";

		if (hasId(openParentheses)) {

			featureID = line.substring(openParentheses + 1, closeParentheses);
			featureName = line.substring(lineAfterTabs + 2, openParentheses).trim();

			if (doesNotHaveAName(featureName)) {
				featureName = featureID;
			}
			
		} else if (isAGroup(openBrackets)) {

			featureName = line.substring(lineAfterTabs + 2, openBrackets).trim();

			if (doesNotHaveAName(featureName)) {
				featureName = "_id_" + idCounter++;
				featureID = featureName.replace(' ', '_');
			}
			// does not have ID, has name: id = name or id = automatic
			else {
				if (idCreationStrategy == USE_VARIABLE_NAME_AS_ID) {
					featureID = featureName.replace(' ', '_');
				} else {
					featureID = "_id_" + idCounter++;
				}
			}
		}
		// does not have ID, is not a group
		else {
			featureName = line.substring(lineAfterTabs + 2).trim();
			// does not have ID, doesn't have name: name = ID = automatic
			if (doesNotHaveAName(featureName)) {
				featureName = "_id_" + idCounter++;
				featureID = featureName.replace(' ', '_');
			}
			// does not have ID, has name: id = name or id = automatic
			else {
				if (idCreationStrategy == USE_VARIABLE_NAME_AS_ID) {
					featureID = featureName.replace(' ', '_');
				} else {
					featureID = "_id_" + idCounter++;
				}
			}
		}

		if (isRoot(nodeType)) {
			node = new RootNode(featureID, featureName, TreeNodeRendererFactory.createRootRenderer());
		} else if (isMandatory(nodeType)) {
			node = new SolitaireFeature(false, featureID, featureName, TreeNodeRendererFactory.createMandatoryRenderer());
		} else if (isOptional(nodeType)) {
			node = new SolitaireFeature(true, featureID, featureName, TreeNodeRendererFactory.createOptionalRenderer());
		} else if (isGroup(nodeType)) {

			int openBracketsGroup = line.indexOf('[', lineAfterTabs);
			int commaGroup = line.indexOf(',', openBracketsGroup);
			int closeBracketsGroup = line.indexOf(']', commaGroup);

			int min = 1;
			int max = -1;

			min = getMinAndVerifyCardinalityLowerbound(line, openBracketsGroup, commaGroup, min);
			max = getMaxAndVerifyCardinalityUpperBound(line, commaGroup, closeBracketsGroup, max);

			node = new FeatureGroup(featureID, featureName, min, max, TreeNodeRendererFactory.createFeatureGroupRenderer());
			//node = new FeatureGroup(featureID, "", min, max, TreeNodeRendererFactory.createFeatureGroupRenderer());
		}
		// It's assumed a GROUPED feature
		else {
			node = new GroupedFeature(featureID, featureName, TreeNodeRendererFactory.createGroupedRenderer());
		}

		String decisionStepString = line.substring(firstVerticalBar + 1, secondVerticalBar).trim();
		Integer decisionStepInteger = Integer.parseInt(decisionStepString);
		node.assignValue(decisionStepInteger.intValue());
		
		setDecisionStepWithdefaultValue(node);
		
		String decisionType = line.substring(secondVerticalBar + 1, secondVerticalBar + 3).trim();
		setDecisionType(node, decisionType);

		return node;
	}

	private void verifyIfIsValid(String line, String nodeType) throws FeatureModelException {
		if (isInvalid(nodeType)) {
			throw new FeatureModelException(
					"Error parsing Feature Tree on line '"
							+ line.trim()
							+ "' (invalid node type). Valid node types are :r (root), :m (mandatory), :o (optional), :g (group), and : (grouped)");
		}
	}

	private void setDecisionStepWithdefaultValue(FeatureTreeNode node) {
		node.setProperty("decisionStep", "1");
	}

	private void setDecisionType(FeatureTreeNode node, String decisionType) {
		if (isAutoCompletion(decisionType)) {
			node.setProperty("decisionType", "auto-completion");
		}
		if (isManual(decisionType)) {
			node.setProperty("decisionType", "manual");
		}
		if (isPropagated(decisionType)) {
			node.setProperty("decisionType", "propagated");
		}
	}

	private boolean isPropagated(String decisionType) {
		return decisionType.equals("P");
	}

	private boolean isManual(String decisionType) {
		return decisionType.equals("M");
	}

	private boolean isAutoCompletion(String decisionType) {
		return decisionType.equals("A");
	}

	private int getMaxAndVerifyCardinalityUpperBound(String line, int index2, int index3, int max) throws FeatureModelException {
		try {
			String upperBound = line.substring(index2 + 1, index3).trim();
			if (upperBound.equals("*")) {
				max = -1;
			} else {
				max = Integer.parseInt(upperBound);
			}
		} catch (NumberFormatException ex) {
			throw new FeatureModelException("Error parsing Feature Tree on line '" + line.trim()
					+ "' (invalid cardinality upper bound value). It must be a valid integer.");
		}
		return max;
	}

	private int getMinAndVerifyCardinalityLowerbound(String line, int index1, int index2, int min) throws FeatureModelException {
		try {
			min = Integer.parseInt(line.substring(index1 + 1, index2).trim());
		} catch (NumberFormatException ex) {
			throw new FeatureModelException("Error parsing Feature Tree on line '" + line.trim()
					+ "' (invalid cardinality lower bound value). It must be a valid integer.");
		}
		return min;
	}

	private boolean isGroup(String nodeType) {
		return nodeType.compareToIgnoreCase("G") == 0;
	}

	private boolean isOptional(String nodeType) {
		return nodeType.compareToIgnoreCase("O") == 0;
	}

	private boolean isMandatory(String nodeType) {
		return nodeType.compareToIgnoreCase("M") == 0;
	}

	private boolean isRoot(String nodeType) {
		return nodeType.compareToIgnoreCase("R") == 0;
	}

	private boolean doesNotHaveAName(String featureName) {
		return featureName.length() == 0;
	}

	private boolean hasId(int openParentheses) {
		return openParentheses != -1;
	}

	private boolean isAGroup(int openBrackets) {
		return openBrackets != -1;
	}

	private boolean isInvalid(String nodeType) {
		return nodeType.length() > 0 && !nodeType.equals("R") && !nodeType.equals("O") && !isManual(nodeType)
				&& !nodeType.equals("G");
	}

	private String skipBlanks(LineNumberReader reader) throws IOException {
		String line = reader.readLine();
		while (line != null && doesNotHaveAName(line)) {
			line = reader.readLine();
		}
		return line;
	}

	protected void saveNodes() {
	}
}
